iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Modern Web

Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具系列 第 23

Day 23: 整合與渲染:將探索結果動態生成 UI

  • 分享至 

  • xImage
  •  

我們繼續新的的內容之前,我想先回顧我們過去幾天的內容,其實在前幾天的內容中我們就已經實現了將探索結果動態生成 UI的程式碼

  • Day 21,我們使用 getPrimaryServices() 探索服務後,立刻就整合createServiceCard 函式,將服務渲染成了 UI 卡片。

  • Day 22,我們使用 getCharacteristics() 探索特徵後,也立刻整合renderCharacteristic 函式,將特徵渲染成了互動面板。

在快速寫完這些環環相扣的程式碼後,我們有必要停下腳步,進行一次全面的「整合回顧」

因此,今天我們不學習新的 API。相反地,我們將扮演「總工程師」的角色,審視我們已經建好的這條從「探索」到「渲染」的自動化流水線,徹底理解數據是如何在其中流動,以及各個模組是如何完美協作的。這將鞏固我們的知識,為接下來的數據讀寫操作,打下最堅實的理解基礎。


1. 我們的「探索與渲染」流水線全貌

到昨天為止,我們的 scanButton.onclick 函式已經成為了整個專案的引擎。讓我們第一次將它完整的樣貌呈現出來,看看這條流水線的宏偉結構。

 //app.js
scanButton.onclick = async () => {
  // 步驟 0: 初始化
  log('初始化...');
  gattContainer.innerHTML = '';
  logContainer.innerHTML = '';
  // ... 清空 gattProfile ...

  try {
    // 步驟 1: 請求裝置 (觸發 UI 互動)
    log('正在請求藍牙裝置...');
    const device = await navigator.bluetooth.requestDevice({ acceptAllDevices: true });
    log(`> 已選擇裝置: ${device.name || `ID: ${device.id}`}`);
    gattProfile.device = device;

    // 步驟 2: 連接 GATT 伺服器
    log('正在連接到 GATT 伺服器...');
    const server = await device.gatt.connect();
    log('> 成功連接到 GATT 伺服器!');
    gattProfile.server = server;
    statusText.textContent = `已連接至 ${device.name}`;

    // 步驟 3: 探索所有服務
    log('正在探索服務...');
    const services = await server.getPrimaryServices();
    log(`> 發現 ${services.length} 個主要服務!`);

    // 步驟 4: 遍歷服務,並為每個服務探索其所有特徵
    for (const service of services) {
      log(`>> 正在處理服務: ${service.uuid}`);
      
      // 4a. 更新資料模型 (服務層)
      gattProfile.services[service.uuid] = {
        uuid: service.uuid,
        instance: service,
        uiCard: null, // 先佔位
        characteristics: {}
      };
      
      // 4b. 渲染 UI (服務層)
      const serviceCard = createServiceCard({ uuid: service.uuid, name: `服務` });
      gattContainer.appendChild(serviceCard);
      gattProfile.services[service.uuid].uiCard = serviceCard;

      // 步驟 5: 探索該服務下的所有特徵
      log(`---> 正在探索特徵...`);
      const characteristics = await service.getCharacteristics();
      log(`---> 發現 ${characteristics.length} 個特徵!`);

      // 步驟 6: 遍歷特徵
      for (const characteristic of characteristics) {
        log(`-----> 特徵 UUID: ${characteristic.uuid}`);
        
        // 6a. 更新資料模型 (特徵層)
        gattProfile.services[service.uuid].characteristics[characteristic.uuid] = {
          uuid: characteristic.uuid,
          properties: characteristic.properties,
          instance: characteristic,
          value: null
        };

        // 6b. 渲染 UI (特徵層)
        renderCharacteristic({
          uuid: characteristic.uuid,
          properties: characteristic.properties
        }, serviceCard);
      }
    }
    log('所有服務與特徵探索完畢!');

  } catch(error) {
    log(`錯誤: ${error.message}`);
  }
};

2. 數據的旅程:從藍牙信號到網頁介面

上面的程式碼看似複雜,但數據的流動路徑其實非常清晰,就像一條河的流向:

  1. 源頭 (API)

    • requestDevice, getPrimaryServices, getCharacteristics 這些非同步 API 就像是河流的源頭,它們從「真實世界」中捕獲原始的藍牙數據(device, service, characteristic 物件)。
  2. 水庫 (Data Model - gattProfile)

    • 在得到原始數據後,我們做的第一件事,就是將這些數據進行結構化處理,並存入我們的「中央資料庫」——gattProfile 物件。

    • gattProfile 物件就像一個巨大的水庫,它不關心這些水未來要用來發電還是灌溉,它只負責忠實地、有條理地儲存所有獲取到的資訊。

  3. 引水渠道 (Function Call)

    • 在將數據存入「水庫」後,我們立刻呼叫我們的 UI 生成函式(createServiceCard, renderCharacteristic)。

    • 我們從「水庫」中取出需要的數據(例如 uuid, properties),作為參數,透過這個「引水渠道」傳遞給 UI 工廠。

  4. 水龍頭 (UI Factories)

    • createServiceCardrenderCharacteristic 這兩個函式,就像是終端的水龍頭。它們是「純粹的」:它們不關心藍牙,不關心 API,它們唯一的任務就是接收傳入的數據,並根據這些數據,「渲染」出對應的 HTML 元素(DOM 物件)。
  5. 目的地 (DOM)

    • 最後,UI 工廠生產出的 HTML 元素,透過 appendChild 被掛載到 DOM 上,最終呈現在使用者的螢幕上。

這個從「獲取 -> 儲存 -> 渲染」的單向數據流,是現代前端開發中最核心、最穩定的架構模式之一。


3. 我們架構的兩大支柱

透過這次回顧,我們可以清晰地看到,我們下意識地建立了一個非常健康、易於維護的程式碼架構,它依賴於兩大支柱:

  • 支柱一:gattProfile - 單一事實來源 (Single Source of Truth)

    • 任何關於這個藍牙裝置的狀態,我們都應該只信任 gattProfile 物件中的紀錄。它就是我們應用的「大腦記憶區」。
  • 支柱二:UI 工廠 - 數據與視圖分離 (Separation of Concerns)

    • 我們的 UI 生成函式與藍牙邏輯是解耦的。這意味著,未來如果我們想修改 UI 的外觀(例如把按鈕變大,卡片換顏色),我們只需要去修改 createServiceCardrenderCharacteristic 函式,完全不需要觸碰任何藍牙 API 相關的程式碼,反之亦然。這使得維護和擴展變得非常容易。

總結與後續

今天,我們沒有學習新知,而是進行了一次更有價值的全局回顧

現在,我們的探索階段已經畫上了完美的句號。我們的應用程式已經可以像一個專業的偵錯工具一樣,完整地、動態地、即時地映射出任何一個 BLE 裝置的內部結構。

地圖已經繪製完成,寶箱的位置也已全部標出。從明天開始,我們將正式進入數據互動的激動人心的階段。我們將拿起工具箱中的第一個工具——「讀取」,學習使用 characteristic.readValue(),第一次真正地從寶箱中取出數據,並讓它顯示在我們的介面上!

那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。


上一篇
Day 22:專案核心 (4):`getCharacteristics()` 動態探索所有特徵
下一篇
Day 24:綁定事件 (1):為動態生成的「讀取」按鈕注入靈魂
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言